package com.iqengines.sdk;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.TimeZone;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Hex;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

import android.util.Log;
	

public class IQRemote implements Serializable {
    
	private String timeStamp;
	
    public final static int MAX_IMAGE_SIZE = 480;

    private static final String TAG = IQRemote.class.getName();
    
    /*
     * CONSTRUCTORS
     */
    
    /**
     * Constructor
     *
     * @param key
     *            A non-<code>null</code> {@link String} : Your API key.
     * @param secret
     *            A non-<code>null</code> {@link String} : Your API secret.
     */
    public IQRemote(String key, String secret) {
        this.key = key;
        this.secret = secret;
    }

    /*
     * PUBLIC METHODS
     */
    
    /**
     * Upload an image to the IQ Engines' server.
     *
     * @param image
     *            A non-<code>null</code> {@link File} : The image file you
     *            would like to get labels for. The image's height and width
     *            should be less than 640 pixels.
     * @param deviceId
     * 			  Device unique identifier
     * @return A non-<code>null</code> {@link IQEQuery}
     * @throws IOException 
     */
    public IQEQuery query(File image, String deviceId, String qid) throws IOException {

        
        return query(image, null, null, deviceId, true, null, null, null,qid);
    }
    
    public String DefineQueryId(File image, String webhook, String extra,
            String device_id, boolean json,
            String gps_altitude, String gps_longitude,
            String gps_latitude) throws IOException {
        TreeMap<String, String> fields = new TreeMap<String, String>();

        // Optional parameters
        if (webhook != null) {
            fields.put("webhook", webhook);
        }
        if (extra != null) {
            fields.put("extra", extra);
        }
        if (device_id != null) {
            fields.put("device_id", device_id);
        }
        if (json) {
            fields.put("json", "1");
        }
        if (gps_altitude != null) {
            fields.put("gps_altitude", gps_altitude);
        }
        if (gps_longitude != null) {
            fields.put("gps_longitude", gps_longitude);
        }
        if (gps_latitude != null) {
            fields.put("gps_latitude", gps_latitude);
        }

        // Required parameters
        fields.put("img", image.getPath());
        timeStamp=now();
        fields.put("time_stamp", timeStamp);
        fields.put("api_key", key);
        
        return buildSignature(fields);
    }

    /**
     * Upload an image to the IQ Engines' server.
     *
     * @param image
     *            A non-<code>null</code> {@link File} : The image file you
     *            would like to get labels for. The image's height and width
     *            should be less than 640 pixels.
     * @param webhook
     *            A possibly-<code>null</code> {@link String} : The URL where
     *            the results are sent via HTTP POST once the labels have been
     *            computed.
     * @param extra
     *            A possibly-<code>null</code> {@link String} : A string that
     *            is posted back when the webhook is called. It is useful for
     *            passing JSON-encoded extra parameters about the query that
     *            might be needed in your application to process the labels.
     * @param device_id
     *            A possibly-<code>null</code> {@link String} : The unique
     *            identification of the device that is querying the API.
     * @param json
     *            If this parameter is true, the results are in the JSON format,
     *            otherwise the results are in the XML format.
     * @param gps_altitude
     *            A possibly-<code>null</code> {@link String} : The altitude of
     *            your GPS coordinates.
     * @param gps_longitude
     *            A possibly-<code>null</code> {@link String} : The longitude of
     *            your GPS coordinates.
     * @param gps_latitude
     *            A possibly-<code>null</code> {@link String} : The latitude of
     *            your GPS coordinates.
     * @return A non-<code>null</code> {@link IQEQuery}
     * @throws IOException 
     */
    public IQEQuery query(File image, String webhook, String extra,
            String device_id, boolean json,
            String gps_altitude, String gps_longitude,
            String gps_latitude, String queryId) throws IOException {
        TreeMap<String, String> fields = new TreeMap<String, String>();

        // Optional parameters
        if (webhook != null) {
            fields.put("webhook", webhook);
        }
        if (extra != null) {
            fields.put("extra", extra);
        }
        if (device_id != null) {
            fields.put("device_id", device_id);
        }
        if (json) {
            fields.put("json", "1");
        }
        if (gps_altitude != null) {
            fields.put("gps_altitude", gps_altitude);
        }
        if (gps_longitude != null) {
            fields.put("gps_longitude", gps_longitude);
        }
        if (gps_latitude != null) {
            fields.put("gps_latitude", gps_latitude);
        }

        // Required parameters
        fields.put("img", image.getPath());
        fields.put("time_stamp", timeStamp);
        fields.put("api_key", key);
        fields.put("api_sig", queryId);

        if (image.exists()) {
            return new IQEQuery(post(IQESelector.query, fields), fields.get("api_sig"));
        } else {
            return new IQEQuery("Error : File '" + image.getPath() + "' doesn't exists", fields.get("api_sig"));
        }
    }

    
    /**
     * The Update API is a long polling request to our server that returns a
     * list of qids along with the labels that have been successfully processed
     * by our image labeling engine. The Update API times out after 90 seconds.
     *
     * @return A non-<code>null</code> {@link String}
     * @throws IOException 
     */
    
    
    public String update() throws IOException {
        return update(null, false);
    }

    
    /**
     * The Update API is a long polling request to our server that returns a
     * list of qids along with the labels that have been successfully processed
     * by our image labeling engine. The Update API times out after 90 seconds.
     *
     * @param device_id
     *            A possibly-<code>null</code> {@link String} : The unique
     *            identification of the device that is querying the API. If you
     *            are using the API on multiple mobile devices, you should pass
     *            the device_id as a parameter to the Query API and Update API.
     *            This ensures that the Update API returns only results
     *            corresponding to image queries sent by the device.
     * @param json
     *            If this parameter is true, the results are in the JSON format,
     *            otherwise the results are in the XML format.
     *
     * @return A non-<code>null</code> {@link String}
     * @throws IOException 
     */
    
    
    public String update(String device_id, boolean json) throws IOException {
        TreeMap<String, String> fields = new TreeMap<String, String>();

        // Optional parameters
        if (device_id != null) {
            fields.put("device_id", device_id);
        }
        if (json) {
            fields.put("json", "1");
        }
        // Required parameters
        fields.put("time_stamp", now());
        fields.put("api_key", key);
        fields.put("api_sig", buildSignature(fields));

        return post(IQESelector.update, fields);
    }

    
    /**
     * The Upload API allows you to upload images to the IQ Engines object 
     * search database through a RESTful interface. Once the computer vision
     * system has successfully indexed your image, you will be able to search
     * for it using the query api.
     * 
     * @param images
     *          A non-<code>null</code> {@link ArrayList} of {@link File} : A 
     *          set of image files you would like to associate to this object.
     * @param name
     *          A non-<code>null</code> {@link String} : The human-readable name 
     *          of the object. This is what we return when you use the query
     *          api.
     *
     * @return A non-<code>null</code> {@link String}
     * @throws IOException 
     */
    
    
    public String upload(ArrayList<File> images, String name) throws IOException {
        return upload(images, name, null, null, false, null);
    }

    
    /**
     * The Upload API allows you to upload images to the IQ Engines object
     * search database through a RESTful interface. Once the computer vision
     * system has successfully indexed your image, you will be able to search
     * for it using the query api.
     *
     * @param images
     *          A non-<code>null</code> {@link ArrayList} of {@link File} : A 
     *          set of image files you would like to associate to this object.
     * @param name
     *          A non-<code>null</code> {@link String} : The human-readable name 
     *          of the object. This is what we return when you use the query
     *          api.
     * @param custom_id
     *          A possibly-<code>null</code> {@link String} : A unique id within
     *          the collection you are uploading. This custom_id can be used to
     *          reference this object’s meta data at a later point in time.
     * @param meta
     *          A possibly-<code>null</code> {@link String} : This is a
     *          json-dictionary encoded as a string of additional meta-data to
     *          be linked to the uploaded image.
     *          (e.g. {‘isbn’: ‘9780393326291’})
     * @param json
     *          If this parameter is true, the results are in the JSON format,
     *          otherwise the results are in the XML format.
     * @param collection
     *          A possibly-<code>null</code> {@link String} : This field is a
     *          string that ties multiple objects together into a ‘collection’.
     *          You can use this field to more easily look up objects belonging
     *          to certain groups later.
     *
     * @return A non-<code>null</code> {@link String}
     * @throws IOException 
     */
    
    
    public String upload(ArrayList<File> images, String name, String custom_id, String meta, boolean json, String collection) throws IOException {
        TreeMap<String, String> fields = new TreeMap<String, String>();

        // Optional parameters
        if (custom_id != null) {
            fields.put("custom_id", custom_id);
        }
        if (meta != null) {
            fields.put("meta", meta);
        }
        if (collection != null) {
            fields.put("collection", collection);
        }
        if (json) {
            fields.put("json", "1");
        }

        // Required parameters
        int index = 0;
        for (File image : images) {
            if (!image.exists()) {
                return "Error : File '" + image.getPath() + "' doesn't exists";
            }
            fields.put("images" + ++index, image.getPath());
        }
        fields.put("name", name);
        fields.put("time_stamp", now());
        fields.put("api_key", key);
        fields.put("api_sig", buildSignature(fields));


        return post(IQESelector.object, fields);
    }

    
    /**
     * The Result API is used to retrieve the labels
     *
     * @param qid
     *            A non-<code>null</code> {@link String} : The unique identifier
     *            of the image for which you want to retrieve the results.
     * @param json
     *            If this parameter is true, the results are in the JSON format,
     *            otherwise the results are in the XML format.
     *
     * @return A non-<code>null</code> {@link String}
     * @throws IOException 
     */
    
    
    public String result(String qid, boolean json) throws IOException {
        TreeMap<String, String> fields = new TreeMap<String, String>();

        // Optional parameter
        if (json) {
            fields.put("json", "1");
        }

        // Required parameters
        fields.put("time_stamp", now());
        fields.put("api_key", key);
        fields.put("qid", qid);
        fields.put("api_sig", buildSignature(fields));

        return post(IQESelector.result, fields);
    }

    /*
     * PRIVATE METHODS
     */
    
    
    /**
     * Returns the current time stamp using the following formatting :
     * "YYYYmmDDHHMMSS"
     *
     * @return a non null {@link String}
     */
    
    
    private String now() {
    	
        Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        String date_format = "yyyyMMddkkmmss";
        SimpleDateFormat sdf = new SimpleDateFormat(date_format);
        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
 
        return sdf.format(c.getTime());
    }

    
    /**
     * Computes the signature of the API call
     *
     * @param fields
     *            A non-<code>null</code> {@link TreeMap} that contains the
     *            arguments of the request.
     * @return A non-<code>null</code> {@link String} : The message
     *         authentication code
     */
    
    
    private String buildSignature(TreeMap<String, String> fields) {
        // join key value pairs together
        String result = null;
        String raw_string = "";
        Iterator<String> i = fields.keySet().iterator();
        while (i.hasNext()) {
            String tmpKey = i.next();
            String value = fields.get(tmpKey);
            if (tmpKey.equals("img")) {
                // if the argument is an image, only keep the name of the file
                File image = new File(value);
                raw_string += "img" + image.getName();
            } else if (tmpKey.startsWith("images")) {
                // if the argument is an image, only keep the name of the file
                File image = new File(value);
                raw_string += "images" + image.getName();
            } else {
                raw_string += tmpKey + value;
            }
        }
        try {
            String HMAC_SHA1_ALGORITHM = "HmacSHA1";
            byte[] secret_bytes = secret.getBytes();
            SecretKeySpec secret_key = new SecretKeySpec(secret_bytes, HMAC_SHA1_ALGORITHM);

            Mac m = Mac.getInstance("HmacSHA1");
            m.init(secret_key);
            byte[] signature_raw = m.doFinal(raw_string.getBytes());

            // Convert raw bytes to Hex
            byte[] signature_hex = new Hex().encode(signature_raw);

            //  Covert array of Hex bytes to a String
            result = new String(signature_hex, "ISO-8859-1");
        } catch (NoSuchAlgorithmException e) {
            return "Exception : NoSuchAlgorithmException" + e.getMessage();
        } catch (InvalidKeyException e) {
            return "Exception : InvalidKeyException" + e.getMessage();
        } catch (UnsupportedEncodingException e) {
            return "Exception : UnsupportedEncodingException" + e.getMessage();
        }
        return result;
    }

    
    /**
     * Post fields and files to an http host as multipart/form-data.
     *
     * @param selector
     *            A non-<code>null</code> {@link IQESelector} : The type of 	
     *            message.
     * @param fields
     *            A non-<code>null</code> {@link TreeMap} : The fields
     * @return A non-<code>null</code> {@link String} : The server's response.
     * @throws IOException 
     */
    
    
    private String post(IQESelector selector, TreeMap<String, String> fields) throws IOException {
    	
        String result = "error";
        String url = "http://api.iqengines.com/v1.2/" + selector + "/";

        HttpClient client = new DefaultHttpClient();
        HttpPost httppost = new HttpPost(url);
        MultipartEntity entity = new MultipartEntity();

        Iterator<String> i = fields.keySet().iterator();
        while (i.hasNext()) {
            String tmpKey = i.next();
            if (tmpKey.equals("img")) {
                File img = new File(fields.get(tmpKey));
                entity.addPart(tmpKey, new FileBody(img));
            } else if (tmpKey.startsWith("images")) {
                File img = new File(fields.get(tmpKey));
                entity.addPart("images", new FileBody(img));
            } else {
                entity.addPart(tmpKey, new StringBody(fields.get(tmpKey)));
            }
        }
        
        httppost.setEntity(entity);
        HttpResponse response = client.execute(httppost);
        HttpEntity resEntity = response.getEntity();
        
        if (resEntity != null) {
        	Log.d(TAG,"resEntity is not null");
            long length = resEntity.getContentLength();
            // Check if we have to stream the result of the query
            if (length != -1 && length < 2048) {
                result = EntityUtils.toString(resEntity);
                Log.d(TAG, "result :"+result);
                
            } else {
                InputStream instream = resEntity.getContent();

                Writer writer = new StringWriter();
                char[] buffer = new char[1024];

                try {
                    Reader reader = new BufferedReader(new InputStreamReader(instream));

                    int n;
                    while ((n = reader.read(buffer)) != -1) {
                    	Log.d(TAG,""+reader.read(buffer));
                        writer.write(buffer, 0, n);
                    }
                } finally {
                    instream.close();
                }
                result = writer.toString();
            }
            resEntity.consumeContent();
        }
        return result;
    }

    
    /**
     * This class is used to store the results of the Query API : The unique
     * identifier of the image and the server's response.
     *
     * @author Vincent Garrigues
     */
    
    
    public class IQEQuery implements Serializable {

    	
        /**
         * Creates a new {@link IQEQuery}.
         *
         * @param result A non-<code>null</code> {@link String}.
         * @param qid A non-<code>null</code> {@link String}.
         */
    	
    	
        public IQEQuery(String result, String qid) {
            this.result = result;
            this.qid = qid;
        }

        
        /**
         * The server's response.
         *
         * @return A non-<code>null</code> {@link String}.
         */
        
        
        public String getResult() {
            return result;
        }

        
        /**
         * The unique identifier of the image for which you want to retrieve the
         * results.
         *
         * @return A non-<code>null</code> {@link String}.
         */
        
        
        public String getQID() {
            return qid;
        }
        /** The result */
        private String result;
        /** The query id */
        private String qid;
        /** Generated serial id */
        private static final long serialVersionUID = 1930709349669617215L;
    }

    
    /**
     * An enumeration for the different urls.
     *
     * @author Vincent Garrigues
     */
    
    
    public enum IQESelector {

        query,
        update,
        result,
        object
    }
    
    /** Your API key */
    
    private final String key;

    /** Your API secret */
    
    private final String secret;
    
    /** Generated serial id */
    
    private static final long serialVersionUID = -8870882783562183990L;
}
